package jmpo.cls;

import java.lang.reflect.Proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Optional;

/**
 * В отличие от языков, компилируемых в машинный код, JVM может позволить себе
 * сохранять большую часть информации о типах, включая внутренние данные и 
 * информацию о методах. Т.е. на уровне JVM сохраняется структура классов.
 * Существует два понятия, которые связаны с получением информации о классах
 * во время исполнения программы:
 *     1) интроспекция - собственно получение внутренней информации об объекте в период исполнения
 *     2) рефлекция - позволяет не только получать информацию, но и вносить изменения в код
 * 
 * В Java, а точнее уже в JVM, существует специальный JavaReflectionAPI, выраженный в Java 
 * Reflection Class.
 * 
 * Хотя с точки зрения статической типизации рефлекция и интроспекция - это некорректная работа,
 * рефлексии очень востребованы в задачах:
 *     1) тестирования, когда нужно получить доступ к внутренним данным или протестировать работу закрытых частей класса;
 *     2) сериализации и десериализации, в этих случаях тоже может потребоваться доступ к закрытым данным для более удобного кода;
 *     3) при создании экстенсивных фреймворков, например, Spring или Hibernate, здесь тоже необходимо получать данные о содержимом классов.
 */

/**
 * Пример простейшего анализа класса и доступа к закрытым компонентам.
 */
final class AuthorData {
    public String getName() {
        return name;
    }

    public long getTaxId() {
        return taxId;
    }

    private final String name;
    private final long taxId;

    private AuthorData(String name, long taxId) {
        this.name = name;
        this.taxId = taxId;
    }
}
/**
 * Класс выше устроен таким образом, что мы не можем явно его конструировать.
 * Но мы можем сделать это через рефлекцию. 
 */
final class AuthorDataReflectionFactory {
    static Class<AuthorData> authorDataClass;
    static { // static initializer
        authorDataClass = AuthorData.class;
    }

    public static Optional<AuthorData> instance(String name, long taxId) {
        try {
                // сначала мы создаём массив типов, для которых хотим вызвать конструктор
                // это нужно так как класс может иметь несколько перегруженных конструкторов
            Class<?>[] parameterTypes = {String.class, Long.TYPE}; // для фундаментальных типов мы получаем класс-тип через их обёртки
                // находим конструктор, если он есть, если нет -> NoSuchMethodException
            var constructor = authorDataClass.getDeclaredConstructor(parameterTypes);
                // подавляем проверки доступа, иначе -> IllegalAccessException
            constructor.setAccessible(true);
                // создаём новый экземпляр
                // если создать невозможно -> InstantiationException
                // если создаётся с исключением -> оно оборачивается в checked InvocationTargetException
            return Optional.of(constructor.newInstance(new Object[] {name, taxId}));
        } catch (NoSuchMethodException e) {
            // если такого конструктора вообще нет
        } catch (IllegalAccessException e) {
            // например попытка вызвать приватный конструктор без подавления проверок доступа
        } catch  (InstantiationException e) {
            // невозможно инстанцировать, например абстрактный класс
        } catch (InvocationTargetException e) {
            // оборачивает любое исключение в конструкторе, реальное исключение находится в getCause
        } 
        return Optional.empty();
    }
}
/**
 * Стратегия создания объекта через рефлексию может помочь нам отделить сам класс, 
 * от логики его инстанцирования. Типичной задачей является ситуация Singleton. 
 * Мы можем создать специальную фабрику, которая будет обслуживать сложный процесс 
 * создания объекта, при том сам объект даже не будет об этом знать.
 */
/**
 * Рефлекции включают в себя классы
 * Class        - информация о классе
 * Method       - информация о методе
 * Field        - информация о поле
 * Constructor  - информация о конструкторе
 */

/**
 * Стандартная библиотека Java реализует интересный механизм: динамическое прокси.
 * Суть этой идеи состоит в том, прокси класс создаётся в рантайме и не требует описания
 * статическим классом, но может успешно реализоваывать произвольный набор интерфейсов
 * через рефлексию.
 */

interface A {
    void doA();
}

interface B {
    void doB();
}

final class TestInvocationHandler implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("TestableInvocationHandler: " + method.getName());
        return null;
    }
}

final class ProxyTest {
    static void test(String[] interfaceNames) throws ClassNotFoundException {
        var classLoader = ProxyTest.class.getClassLoader();

        Class<?>[] interfaceClasses = new Class<?>[interfaceNames.length];
        for (int idx = 0; idx != interfaceNames.length; ++idx)
            interfaceClasses[idx] = classLoader.loadClass(interfaceNames[idx]);

        var mock = Proxy.newProxyInstance(classLoader, interfaceClasses, new TestInvocationHandler());
        A aMock = (A)mock;
        aMock.doA();
        B bMock = (B)mock;
        bMock.doB();
    }
}

public class ReflectionClass {
    private static void testReflection() {
        var aOpt = AuthorDataReflectionFactory.instance("H", 1);
        if (aOpt.isPresent()) {
            var a = aOpt.get();
            System.out.println(a.getName() + a.getTaxId());
        }
        else {
            System.err.println("Something has gone wrong during construction");
        }
    }

    private static void testProxy() {
        try {
            ProxyTest.test(new String[] {"jmpo.cls.A", "jmpo.cls.B"});
        } catch(ClassNotFoundException e) {
            System.err.println("Class not found: " + e.getMessage());
        }
    }
    public static void main(String[] args) throws Exception {
        if (false) testReflection();
        if (false) testProxy();
    }
}
